// https://github.com/mozilla/readability/tree/814f0a3884350b6f1adfdebb79ca3599e9806605

/*eslint-env es6:false*/
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
 * This is a relatively lightweight DOMParser that is safe to use in a web
 * worker. This is far from a complete DOM implementation; however, it should
 * contain the minimal set of functionality necessary for Readability.js.
 *
 * Aside from not implementing the full DOM API, there are other quirks to be
 * aware of when using the JSDOMParser:
 *
 *   1) Properly formed HTML/XML must be used. This means you should be extra
 *      careful when using this parser on anything received directly from an
 *      XMLHttpRequest. Providing a serialized string from an XMLSerializer,
 *      however, should be safe (since the browser's XMLSerializer should
 *      generate valid HTML/XML). Therefore, if parsing a document from an XHR,
 *      the recommended approach is to do the XHR in the main thread, use
 *      XMLSerializer.serializeToString() on the responseXML, and pass the
 *      resulting string to the worker.
 *
 *   2) Live NodeLists are not supported. DOM methods and properties such as
 *      getElementsByTagName() and childNodes return standard arrays. If you
 *      want these lists to be updated when nodes are removed or added to the
 *      document, you must take care to manually update them yourself.
 */
(function (global) {

	// XML only defines these and the numeric ones:

	var entityTable = {
		'lt': '<',
		'gt': '>',
		'amp': '&',
		'quot': '"',
		'apos': '\'',
	};

	var reverseEntityTable = {
		'<': '&lt;',
		'>': '&gt;',
		'&': '&amp;',
		'"': '&quot;',
		'\'': '&apos;',
	};

	function encodeTextContentHTML(s) {
		return s.replace(/[&<>]/g, function(x) {
			return reverseEntityTable[x];
		});
	}

	function encodeHTML(s) {
		return s.replace(/[&<>'"]/g, function(x) {
			return reverseEntityTable[x];
		});
	}

	function decodeHTML(str) {
		return str.replace(/&(quot|amp|apos|lt|gt);/g, function(match, tag) {
			return entityTable[tag];
		}).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi, function(match, hex, numStr) {
			var num = parseInt(hex || numStr, hex ? 16 : 10); // read num
			return String.fromCharCode(num);
		});
	}

	// When a style is set in JS, map it to the corresponding CSS attribute
	var styleMap = {
		'alignmentBaseline': 'alignment-baseline',
		'background': 'background',
		'backgroundAttachment': 'background-attachment',
		'backgroundClip': 'background-clip',
		'backgroundColor': 'background-color',
		'backgroundImage': 'background-image',
		'backgroundOrigin': 'background-origin',
		'backgroundPosition': 'background-position',
		'backgroundPositionX': 'background-position-x',
		'backgroundPositionY': 'background-position-y',
		'backgroundRepeat': 'background-repeat',
		'backgroundRepeatX': 'background-repeat-x',
		'backgroundRepeatY': 'background-repeat-y',
		'backgroundSize': 'background-size',
		'baselineShift': 'baseline-shift',
		'border': 'border',
		'borderBottom': 'border-bottom',
		'borderBottomColor': 'border-bottom-color',
		'borderBottomLeftRadius': 'border-bottom-left-radius',
		'borderBottomRightRadius': 'border-bottom-right-radius',
		'borderBottomStyle': 'border-bottom-style',
		'borderBottomWidth': 'border-bottom-width',
		'borderCollapse': 'border-collapse',
		'borderColor': 'border-color',
		'borderImage': 'border-image',
		'borderImageOutset': 'border-image-outset',
		'borderImageRepeat': 'border-image-repeat',
		'borderImageSlice': 'border-image-slice',
		'borderImageSource': 'border-image-source',
		'borderImageWidth': 'border-image-width',
		'borderLeft': 'border-left',
		'borderLeftColor': 'border-left-color',
		'borderLeftStyle': 'border-left-style',
		'borderLeftWidth': 'border-left-width',
		'borderRadius': 'border-radius',
		'borderRight': 'border-right',
		'borderRightColor': 'border-right-color',
		'borderRightStyle': 'border-right-style',
		'borderRightWidth': 'border-right-width',
		'borderSpacing': 'border-spacing',
		'borderStyle': 'border-style',
		'borderTop': 'border-top',
		'borderTopColor': 'border-top-color',
		'borderTopLeftRadius': 'border-top-left-radius',
		'borderTopRightRadius': 'border-top-right-radius',
		'borderTopStyle': 'border-top-style',
		'borderTopWidth': 'border-top-width',
		'borderWidth': 'border-width',
		'bottom': 'bottom',
		'boxShadow': 'box-shadow',
		'boxSizing': 'box-sizing',
		'captionSide': 'caption-side',
		'clear': 'clear',
		'clip': 'clip',
		'clipPath': 'clip-path',
		'clipRule': 'clip-rule',
		'color': 'color',
		'colorInterpolation': 'color-interpolation',
		'colorInterpolationFilters': 'color-interpolation-filters',
		'colorProfile': 'color-profile',
		'colorRendering': 'color-rendering',
		'content': 'content',
		'counterIncrement': 'counter-increment',
		'counterReset': 'counter-reset',
		'cursor': 'cursor',
		'direction': 'direction',
		'display': 'display',
		'dominantBaseline': 'dominant-baseline',
		'emptyCells': 'empty-cells',
		'enableBackground': 'enable-background',
		'fill': 'fill',
		'fillOpacity': 'fill-opacity',
		'fillRule': 'fill-rule',
		'filter': 'filter',
		'cssFloat': 'float',
		'floodColor': 'flood-color',
		'floodOpacity': 'flood-opacity',
		'font': 'font',
		'fontFamily': 'font-family',
		'fontSize': 'font-size',
		'fontStretch': 'font-stretch',
		'fontStyle': 'font-style',
		'fontVariant': 'font-variant',
		'fontWeight': 'font-weight',
		'glyphOrientationHorizontal': 'glyph-orientation-horizontal',
		'glyphOrientationVertical': 'glyph-orientation-vertical',
		'height': 'height',
		'imageRendering': 'image-rendering',
		'kerning': 'kerning',
		'left': 'left',
		'letterSpacing': 'letter-spacing',
		'lightingColor': 'lighting-color',
		'lineHeight': 'line-height',
		'listStyle': 'list-style',
		'listStyleImage': 'list-style-image',
		'listStylePosition': 'list-style-position',
		'listStyleType': 'list-style-type',
		'margin': 'margin',
		'marginBottom': 'margin-bottom',
		'marginLeft': 'margin-left',
		'marginRight': 'margin-right',
		'marginTop': 'margin-top',
		'marker': 'marker',
		'markerEnd': 'marker-end',
		'markerMid': 'marker-mid',
		'markerStart': 'marker-start',
		'mask': 'mask',
		'maxHeight': 'max-height',
		'maxWidth': 'max-width',
		'minHeight': 'min-height',
		'minWidth': 'min-width',
		'opacity': 'opacity',
		'orphans': 'orphans',
		'outline': 'outline',
		'outlineColor': 'outline-color',
		'outlineOffset': 'outline-offset',
		'outlineStyle': 'outline-style',
		'outlineWidth': 'outline-width',
		'overflow': 'overflow',
		'overflowX': 'overflow-x',
		'overflowY': 'overflow-y',
		'padding': 'padding',
		'paddingBottom': 'padding-bottom',
		'paddingLeft': 'padding-left',
		'paddingRight': 'padding-right',
		'paddingTop': 'padding-top',
		'page': 'page',
		'pageBreakAfter': 'page-break-after',
		'pageBreakBefore': 'page-break-before',
		'pageBreakInside': 'page-break-inside',
		'pointerEvents': 'pointer-events',
		'position': 'position',
		'quotes': 'quotes',
		'resize': 'resize',
		'right': 'right',
		'shapeRendering': 'shape-rendering',
		'size': 'size',
		'speak': 'speak',
		'src': 'src',
		'stopColor': 'stop-color',
		'stopOpacity': 'stop-opacity',
		'stroke': 'stroke',
		'strokeDasharray': 'stroke-dasharray',
		'strokeDashoffset': 'stroke-dashoffset',
		'strokeLinecap': 'stroke-linecap',
		'strokeLinejoin': 'stroke-linejoin',
		'strokeMiterlimit': 'stroke-miterlimit',
		'strokeOpacity': 'stroke-opacity',
		'strokeWidth': 'stroke-width',
		'tableLayout': 'table-layout',
		'textAlign': 'text-align',
		'textAnchor': 'text-anchor',
		'textDecoration': 'text-decoration',
		'textIndent': 'text-indent',
		'textLineThrough': 'text-line-through',
		'textLineThroughColor': 'text-line-through-color',
		'textLineThroughMode': 'text-line-through-mode',
		'textLineThroughStyle': 'text-line-through-style',
		'textLineThroughWidth': 'text-line-through-width',
		'textOverflow': 'text-overflow',
		'textOverline': 'text-overline',
		'textOverlineColor': 'text-overline-color',
		'textOverlineMode': 'text-overline-mode',
		'textOverlineStyle': 'text-overline-style',
		'textOverlineWidth': 'text-overline-width',
		'textRendering': 'text-rendering',
		'textShadow': 'text-shadow',
		'textTransform': 'text-transform',
		'textUnderline': 'text-underline',
		'textUnderlineColor': 'text-underline-color',
		'textUnderlineMode': 'text-underline-mode',
		'textUnderlineStyle': 'text-underline-style',
		'textUnderlineWidth': 'text-underline-width',
		'top': 'top',
		'unicodeBidi': 'unicode-bidi',
		'unicodeRange': 'unicode-range',
		'vectorEffect': 'vector-effect',
		'verticalAlign': 'vertical-align',
		'visibility': 'visibility',
		'whiteSpace': 'white-space',
		'widows': 'widows',
		'width': 'width',
		'wordBreak': 'word-break',
		'wordSpacing': 'word-spacing',
		'wordWrap': 'word-wrap',
		'writingMode': 'writing-mode',
		'zIndex': 'z-index',
		'zoom': 'zoom',
	};

	// Elements that can be self-closing
	var voidElems = {
		'area': true,
		'base': true,
		'br': true,
		'col': true,
		'command': true,
		'embed': true,
		'hr': true,
		'img': true,
		'input': true,
		'link': true,
		'meta': true,
		'param': true,
		'source': true,
		'wbr': true,
	};

	var whitespace = [' ', '\t', '\n', '\r'];

	// See http://www.w3schools.com/dom/dom_nodetype.asp
	var nodeTypes = {
		ELEMENT_NODE: 1,
		ATTRIBUTE_NODE: 2,
		TEXT_NODE: 3,
		CDATA_SECTION_NODE: 4,
		ENTITY_REFERENCE_NODE: 5,
		ENTITY_NODE: 6,
		PROCESSING_INSTRUCTION_NODE: 7,
		COMMENT_NODE: 8,
		DOCUMENT_NODE: 9,
		DOCUMENT_TYPE_NODE: 10,
		DOCUMENT_FRAGMENT_NODE: 11,
		NOTATION_NODE: 12,
	};

	function getElementsByTagName(tag) {
		tag = tag.toUpperCase();
		var elems = [];
		var allTags = (tag === '*');
		function getElems(node) {
			var length = node.children.length;
			for (var i = 0; i < length; i++) {
				var child = node.children[i];
				if (allTags || (child.tagName === tag))
					elems.push(child);
				getElems(child);
			}
		}
		getElems(this);
		return elems;
	}

	var Node = function () {};

	Node.prototype = {
		attributes: null,
		childNodes: null,
		localName: null,
		nodeName: null,
		parentNode: null,
		textContent: null,
		nextSibling: null,
		previousSibling: null,

		get firstChild() {
			return this.childNodes[0] || null;
		},

		get firstElementChild() {
			return this.children[0] || null;
		},

		get lastChild() {
			return this.childNodes[this.childNodes.length - 1] || null;
		},

		get lastElementChild() {
			return this.children[this.children.length - 1] || null;
		},

		appendChild: function (child) {
			if (child.parentNode) {
				child.parentNode.removeChild(child);
			}

			var last = this.lastChild;
			if (last)
				last.nextSibling = child;
			child.previousSibling = last;

			if (child.nodeType === Node.ELEMENT_NODE) {
				child.previousElementSibling = this.children[this.children.length - 1] || null;
				this.children.push(child);
				child.previousElementSibling && (child.previousElementSibling.nextElementSibling = child);
			}
			this.childNodes.push(child);
			child.parentNode = this;
		},

		removeChild: function (child) {
			var childNodes = this.childNodes;
			var childIndex = childNodes.indexOf(child);
			if (childIndex === -1) {
				throw 'removeChild: node not found';
			} else {
				child.parentNode = null;
				var prev = child.previousSibling;
				var next = child.nextSibling;
				if (prev)
					prev.nextSibling = next;
				if (next)
					next.previousSibling = prev;

				if (child.nodeType === Node.ELEMENT_NODE) {
					prev = child.previousElementSibling;
					next = child.nextElementSibling;
					if (prev)
						prev.nextElementSibling = next;
					if (next)
						next.previousElementSibling = prev;
					this.children.splice(this.children.indexOf(child), 1);
				}

				child.previousSibling = child.nextSibling = null;
				child.previousElementSibling = child.nextElementSibling = null;

				return childNodes.splice(childIndex, 1)[0];
			}
		},

		replaceChild: function (newNode, oldNode) {
			var childNodes = this.childNodes;
			var childIndex = childNodes.indexOf(oldNode);
			if (childIndex === -1) {
				throw 'replaceChild: node not found';
			} else {
				// This will take care of updating the new node if it was somewhere else before:
				if (newNode.parentNode)
					newNode.parentNode.removeChild(newNode);

				childNodes[childIndex] = newNode;

				// update the new node's sibling properties, and its new siblings' sibling properties
				newNode.nextSibling = oldNode.nextSibling;
				newNode.previousSibling = oldNode.previousSibling;
				if (newNode.nextSibling)
					newNode.nextSibling.previousSibling = newNode;
				if (newNode.previousSibling)
					newNode.previousSibling.nextSibling = newNode;

				newNode.parentNode = this;

				// Now deal with elements before we clear out those values for the old node,
				// because it can help us take shortcuts here:
				if (newNode.nodeType === Node.ELEMENT_NODE) {
					if (oldNode.nodeType === Node.ELEMENT_NODE) {
						// Both were elements, which makes this easier, we just swap things out:
						newNode.previousElementSibling = oldNode.previousElementSibling;
						newNode.nextElementSibling = oldNode.nextElementSibling;
						if (newNode.previousElementSibling)
							newNode.previousElementSibling.nextElementSibling = newNode;
						if (newNode.nextElementSibling)
							newNode.nextElementSibling.previousElementSibling = newNode;
						this.children[this.children.indexOf(oldNode)] = newNode;
					} else {
						// Hard way:
						newNode.previousElementSibling = (function() {
							for (var i = childIndex - 1; i >= 0; i--) {
								if (childNodes[i].nodeType === Node.ELEMENT_NODE)
									return childNodes[i];
							}
							return null;
						})();
						if (newNode.previousElementSibling) {
							newNode.nextElementSibling = newNode.previousElementSibling.nextElementSibling;
						} else {
							newNode.nextElementSibling = (function() {
								for (var i = childIndex + 1; i < childNodes.length; i++) {
									if (childNodes[i].nodeType === Node.ELEMENT_NODE)
										return childNodes[i];
								}
								return null;
							})();
						}
						if (newNode.previousElementSibling)
							newNode.previousElementSibling.nextElementSibling = newNode;
						if (newNode.nextElementSibling)
							newNode.nextElementSibling.previousElementSibling = newNode;

						if (newNode.nextElementSibling)
							this.children.splice(this.children.indexOf(newNode.nextElementSibling), 0, newNode);
						else
							this.children.push(newNode);
					}
				} else if (oldNode.nodeType === Node.ELEMENT_NODE) {
					// new node is not an element node.
					// if the old one was, update its element siblings:
					if (oldNode.previousElementSibling)
						oldNode.previousElementSibling.nextElementSibling = oldNode.nextElementSibling;
					if (oldNode.nextElementSibling)
						oldNode.nextElementSibling.previousElementSibling = oldNode.previousElementSibling;
					this.children.splice(this.children.indexOf(oldNode), 1);

					// If the old node wasn't an element, neither the new nor the old node was an element,
					// and the children array and its members shouldn't need any updating.
				}


				oldNode.parentNode = null;
				oldNode.previousSibling = null;
				oldNode.nextSibling = null;
				if (oldNode.nodeType === Node.ELEMENT_NODE) {
					oldNode.previousElementSibling = null;
					oldNode.nextElementSibling = null;
				}
				return oldNode;
			}
		},

		__JSDOMParser__: true,
	};

	for (var nodeType in nodeTypes) {
		Node[nodeType] = Node.prototype[nodeType] = nodeTypes[nodeType];
	}

	var Attribute = function (name, value) {
		this.name = name;
		this._value = value;
	};

	Attribute.prototype = {
		get value() {
			return this._value;
		},
		setValue: function(newValue) {
			this._value = newValue;
		},
		getEncodedValue: function() {
			return encodeHTML(this._value);
		},
	};

	var Comment = function () {
		this.childNodes = [];
	};

	Comment.prototype = {
		__proto__: Node.prototype,

		nodeName: '#comment',
		nodeType: Node.COMMENT_NODE,
	};

	var Text = function () {
		this.childNodes = [];
	};

	Text.prototype = {
		__proto__: Node.prototype,

		nodeName: '#text',
		nodeType: Node.TEXT_NODE,
		get textContent() {
			if (typeof this._textContent === 'undefined') {
				this._textContent = decodeHTML(this._innerHTML || '');
			}
			return this._textContent;
		},
		get innerHTML() {
			if (typeof this._innerHTML === 'undefined') {
				this._innerHTML = encodeTextContentHTML(this._textContent || '');
			}
			return this._innerHTML;
		},

		set innerHTML(newHTML) {
			this._innerHTML = newHTML;
			delete this._textContent;
		},
		set textContent(newText) {
			this._textContent = newText;
			delete this._innerHTML;
		},
	};

	var Document = function (url) {
		this.documentURI = url;
		this.styleSheets = [];
		this.childNodes = [];
		this.children = [];
	};

	Document.prototype = {
		__proto__: Node.prototype,

		nodeName: '#document',
		nodeType: Node.DOCUMENT_NODE,
		title: '',

		getElementsByTagName: getElementsByTagName,

		getElementById: function (id) {
			function getElem(node) {
				var length = node.children.length;
				if (node.id === id)
					return node;
				for (var i = 0; i < length; i++) {
					var el = getElem(node.children[i]);
					if (el)
						return el;
				}
				return null;
			}
			return getElem(this);
		},

		createElement: function (tag) {
			var node = new Element(tag);
			return node;
		},

		createTextNode: function (text) {
			var node = new Text();
			node.textContent = text;
			return node;
		},

		get baseURI() {
			if (!this.hasOwnProperty('_baseURI')) {
				this._baseURI = this.documentURI;
				var baseElements = this.getElementsByTagName('base');
				var href = baseElements[0] && baseElements[0].getAttribute('href');
				if (href) {
					try {
						this._baseURI = (new URL(href, this._baseURI)).href;
					} catch (ex) {/* Just fall back to documentURI */}
				}
			}
			return this._baseURI;
		},
	};

	var Element = function (tag) {
		// We use this to find the closing tag.
		this._matchingTag = tag;
		// We're explicitly a non-namespace aware parser, we just pretend it's all HTML.
		var lastColonIndex = tag.lastIndexOf(':');
		if (lastColonIndex != -1) {
			tag = tag.substring(lastColonIndex + 1);
		}
		this.attributes = [];
		this.childNodes = [];
		this.children = [];
		this.nextElementSibling = this.previousElementSibling = null;
		this.localName = tag.toLowerCase();
		this.tagName = tag.toUpperCase();
		this.style = new Style(this);
	};

	Element.prototype = {
		__proto__: Node.prototype,

		nodeType: Node.ELEMENT_NODE,

		getElementsByTagName: getElementsByTagName,

		get className() {
			return this.getAttribute('class') || '';
		},

		set className(str) {
			this.setAttribute('class', str);
		},

		get id() {
			return this.getAttribute('id') || '';
		},

		set id(str) {
			this.setAttribute('id', str);
		},

		get href() {
			return this.getAttribute('href') || '';
		},

		set href(str) {
			this.setAttribute('href', str);
		},

		get src() {
			return this.getAttribute('src') || '';
		},

		set src(str) {
			this.setAttribute('src', str);
		},

		get srcset() {
			return this.getAttribute('srcset') || '';
		},

		set srcset(str) {
			this.setAttribute('srcset', str);
		},

		get nodeName() {
			return this.tagName;
		},

		get innerHTML() {
			function getHTML(node) {
				var i = 0;
				for (i = 0; i < node.childNodes.length; i++) {
					var child = node.childNodes[i];
					if (child.localName) {
						arr.push('<' + child.localName);

						// serialize attribute list
						for (var j = 0; j < child.attributes.length; j++) {
							var attr = child.attributes[j];
							// the attribute value will be HTML escaped.
							var val = attr.getEncodedValue();
							var quote = (val.indexOf('"') === -1 ? '"' : '\'');
							arr.push(' ' + attr.name + '=' + quote + val + quote);
						}

						if (child.localName in voidElems && !child.childNodes.length) {
							// if this is a self-closing element, end it here
							arr.push('/>');
						} else {
							// otherwise, add its children
							arr.push('>');
							getHTML(child);
							arr.push('</' + child.localName + '>');
						}
					} else {
						// This is a text node, so asking for innerHTML won't recurse.
						arr.push(child.innerHTML);
					}
				}
			}

			// Using Array.join() avoids the overhead from lazy string concatenation.
			// See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
			var arr = [];
			getHTML(this);
			return arr.join('');
		},

		set innerHTML(html) {
			var parser = new JSDOMParser();
			var node = parser.parse(html);
			var i;
			for (i = this.childNodes.length; --i >= 0;) {
				this.childNodes[i].parentNode = null;
			}
			this.childNodes = node.childNodes;
			this.children = node.children;
			for (i = this.childNodes.length; --i >= 0;) {
				this.childNodes[i].parentNode = this;
			}
		},

		set textContent(text) {
			// clear parentNodes for existing children
			for (var i = this.childNodes.length; --i >= 0;) {
				this.childNodes[i].parentNode = null;
			}

			var node = new Text();
			this.childNodes = [ node ];
			this.children = [];
			node.textContent = text;
			node.parentNode = this;
		},

		get textContent() {
			function getText(node) {
				var nodes = node.childNodes;
				for (var i = 0; i < nodes.length; i++) {
					var child = nodes[i];
					if (child.nodeType === 3) {
						text.push(child.textContent);
					} else {
						getText(child);
					}
				}
			}

			// Using Array.join() avoids the overhead from lazy string concatenation.
			// See http://blog.cdleary.com/2012/01/string-representation-in-spidermonkey/#ropes
			var text = [];
			getText(this);
			return text.join('');
		},

		getAttribute: function (name) {
			for (var i = this.attributes.length; --i >= 0;) {
				var attr = this.attributes[i];
				if (attr.name === name) {
					return attr.value;
				}
			}
			return undefined;
		},

		setAttribute: function (name, value) {
			for (var i = this.attributes.length; --i >= 0;) {
				var attr = this.attributes[i];
				if (attr.name === name) {
					attr.setValue(value);
					return;
				}
			}
			this.attributes.push(new Attribute(name, value));
		},

		removeAttribute: function (name) {
			for (var i = this.attributes.length; --i >= 0;) {
				var attr = this.attributes[i];
				if (attr.name === name) {
					this.attributes.splice(i, 1);
					break;
				}
			}
		},

		hasAttribute: function (name) {
			return this.attributes.some(function (attr) {
				return attr.name == name;
			});
		},
	};

	var Style = function (node) {
		this.node = node;
	};

	// getStyle() and setStyle() use the style attribute string directly. This
	// won't be very efficient if there are a lot of style manipulations, but
	// it's the easiest way to make sure the style attribute string and the JS
	// style property stay in sync. Readability.js doesn't do many style
	// manipulations, so this should be okay.
	Style.prototype = {
		getStyle: function (styleName) {
			var attr = this.node.getAttribute('style');
			if (!attr)
				return undefined;

			var styles = attr.split(';');
			for (var i = 0; i < styles.length; i++) {
				var style = styles[i].split(':');
				var name = style[0].trim();
				if (name === styleName)
					return style[1].trim();
			}

			return undefined;
		},

		setStyle: function (styleName, styleValue) {
			var value = this.node.getAttribute('style') || '';
			var index = 0;
			do {
				var next = value.indexOf(';', index) + 1;
				var length = next - index - 1;
				var style = (length > 0 ? value.substr(index, length) : value.substr(index));
				if (style.substr(0, style.indexOf(':')).trim() === styleName) {
					value = value.substr(0, index).trim() + (next ? ' ' + value.substr(next).trim() : '');
					break;
				}
				index = next;
			} while (index);

			value += ' ' + styleName + ': ' + styleValue + ';';
			this.node.setAttribute('style', value.trim());
		},
	};

	// For each item in styleMap, define a getter and setter on the style
	// property.
	for (var jsName in styleMap) {
		(function (cssName) {
			Style.prototype.__defineGetter__(jsName, function () {
				return this.getStyle(cssName);
			});
			Style.prototype.__defineSetter__(jsName, function (value) {
				this.setStyle(cssName, value);
			});
		})(styleMap[jsName]);
	}

	var JSDOMParser = function () {
		this.currentChar = 0;

		// In makeElementNode() we build up many strings one char at a time. Using
		// += for this results in lots of short-lived intermediate strings. It's
		// better to build an array of single-char strings and then join() them
		// together at the end. And reusing a single array (i.e. |this.strBuf|)
		// over and over for this purpose uses less memory than using a new array
		// for each string.
		this.strBuf = [];

		// Similarly, we reuse this array to return the two arguments from
		// makeElementNode(), which saves us from having to allocate a new array
		// every time.
		this.retPair = [];

		this.errorState = '';
	};

	JSDOMParser.prototype = {
		error: function(m) {
			dump('JSDOMParser error: ' + m + '\n');
			this.errorState += m + '\n';
		},

		/**
     * Look at the next character without advancing the index.
     */
		peekNext: function () {
			return this.html[this.currentChar];
		},

		/**
     * Get the next character and advance the index.
     */
		nextChar: function () {
			return this.html[this.currentChar++];
		},

		/**
     * Called after a quote character is read. This finds the next quote
     * character and returns the text string in between.
     */
		readString: function (quote) {
			var str;
			var n = this.html.indexOf(quote, this.currentChar);
			if (n === -1) {
				this.currentChar = this.html.length;
				str = null;
			} else {
				str = this.html.substring(this.currentChar, n);
				this.currentChar = n + 1;
			}

			return str;
		},

		/**
     * Called when parsing a node. This finds the next name/value attribute
     * pair and adds the result to the attributes list.
     */
		readAttribute: function (node) {
			var name = '';

			var n = this.html.indexOf('=', this.currentChar);
			if (n === -1) {
				this.currentChar = this.html.length;
			} else {
				// Read until a '=' character is hit; this will be the attribute key
				name = this.html.substring(this.currentChar, n);
				this.currentChar = n + 1;
			}

			if (!name)
				return;

			// After a '=', we should see a '"' for the attribute value
			var c = this.nextChar();
			if (c !== '"' && c !== '\'') {
				this.error('Error reading attribute ' + name + ', expecting \'"\'');
				return;
			}

			// Read the attribute value (and consume the matching quote)
			var value = this.readString(c);

			node.attributes.push(new Attribute(name, decodeHTML(value)));

			return;
		},

		/**
     * Parses and returns an Element node. This is called after a '<' has been
     * read.
     *
     * @returns an array; the first index of the array is the parsed node;
     *          the second index is a boolean indicating whether this is a void
     *          Element
     */
		makeElementNode: function (retPair) {
			var c = this.nextChar();

			// Read the Element tag name
			var strBuf = this.strBuf;
			strBuf.length = 0;
			while (whitespace.indexOf(c) == -1 && c !== '>' && c !== '/') {
				if (c === undefined)
					return false;
				strBuf.push(c);
				c = this.nextChar();
			}
			var tag = strBuf.join('');

			if (!tag)
				return false;

			var node = new Element(tag);

			// Read Element attributes
			while (c !== '/' && c !== '>') {
				if (c === undefined)
					return false;
				while (whitespace.indexOf(this.html[this.currentChar++]) != -1) {
					// Advance cursor to first non-whitespace char.
				}
				this.currentChar--;
				c = this.nextChar();
				if (c !== '/' && c !== '>') {
					--this.currentChar;
					this.readAttribute(node);
				}
			}

			// If this is a self-closing tag, read '/>'
			var closed = false;
			if (c === '/') {
				closed = true;
				c = this.nextChar();
				if (c !== '>') {
					this.error('expected \'>\' to close ' + tag);
					return false;
				}
			}

			retPair[0] = node;
			retPair[1] = closed;
			return true;
		},

		/**
     * If the current input matches this string, advance the input index;
     * otherwise, do nothing.
     *
     * @returns whether input matched string
     */
		match: function (str) {
			var strlen = str.length;
			if (this.html.substr(this.currentChar, strlen).toLowerCase() === str.toLowerCase()) {
				this.currentChar += strlen;
				return true;
			}
			return false;
		},

		/**
     * Searches the input until a string is found and discards all input up to
     * and including the matched string.
     */
		discardTo: function (str) {
			var index = this.html.indexOf(str, this.currentChar) + str.length;
			if (index === -1)
				this.currentChar = this.html.length;
			this.currentChar = index;
		},

		/**
     * Reads child nodes for the given node.
     */
		readChildren: function (node) {
			var child;
			while ((child = this.readNode())) {
				// Don't keep Comment nodes
				if (child.nodeType !== 8) {
					node.appendChild(child);
				}
			}
		},

		discardNextComment: function() {
			if (this.match('--')) {
				this.discardTo('-->');
			} else {
				var c = this.nextChar();
				while (c !== '>') {
					if (c === undefined)
						return null;
					if (c === '"' || c === '\'')
						this.readString(c);
					c = this.nextChar();
				}
			}
			return new Comment();
		},


		/**
     * Reads the next child node from the input. If we're reading a closing
     * tag, or if we've reached the end of input, return null.
     *
     * @returns the node
     */
		readNode: function () {
			var c = this.nextChar();

			if (c === undefined)
				return null;

			// Read any text as Text node
			var textNode;
			if (c !== '<') {
				--this.currentChar;
				textNode = new Text();
				var n = this.html.indexOf('<', this.currentChar);
				if (n === -1) {
					textNode.innerHTML = this.html.substring(this.currentChar, this.html.length);
					this.currentChar = this.html.length;
				} else {
					textNode.innerHTML = this.html.substring(this.currentChar, n);
					this.currentChar = n;
				}
				return textNode;
			}

			if (this.match('![CDATA[')) {
				var endChar = this.html.indexOf(']]>', this.currentChar);
				if (endChar === -1) {
					this.error('unclosed CDATA section');
					return null;
				}
				textNode = new Text();
				textNode.textContent = this.html.substring(this.currentChar, endChar);
				this.currentChar = endChar + (']]>').length;
				return textNode;
			}

			c = this.peekNext();

			// Read Comment node. Normally, Comment nodes know their inner
			// textContent, but we don't really care about Comment nodes (we throw
			// them away in readChildren()). So just returning an empty Comment node
			// here is sufficient.
			if (c === '!' || c === '?') {
				// We're still before the ! or ? that is starting this comment:
				this.currentChar++;
				return this.discardNextComment();
			}

			// If we're reading a closing tag, return null. This means we've reached
			// the end of this set of child nodes.
			if (c === '/') {
				--this.currentChar;
				return null;
			}

			// Otherwise, we're looking at an Element node
			var result = this.makeElementNode(this.retPair);
			if (!result)
				return null;

			var node = this.retPair[0];
			var closed = this.retPair[1];
			var localName = node.localName;

			// If this isn't a void Element, read its child nodes
			if (!closed) {
				this.readChildren(node);
				var closingTag = '</' + node._matchingTag + '>';
				if (!this.match(closingTag)) {
					this.error('expected \'' + closingTag + '\' and got ' + this.html.substr(this.currentChar, closingTag.length));
					return null;
				}
			}

			// Only use the first title, because SVG might have other
			// title elements which we don't care about (medium.com
			// does this, at least).
			if (localName === 'title' && !this.doc.title) {
				this.doc.title = node.textContent.trim();
			} else if (localName === 'head') {
				this.doc.head = node;
			} else if (localName === 'body') {
				this.doc.body = node;
			} else if (localName === 'html') {
				this.doc.documentElement = node;
			}

			return node;
		},

		/**
     * Parses an HTML string and returns a JS implementation of the Document.
     */
		parse: function (html, url) {
			this.html = html;
			var doc = this.doc = new Document(url);
			this.readChildren(doc);

			// If this is an HTML document, remove root-level children except for the
			// <html> node
			if (doc.documentElement) {
				for (var i = doc.childNodes.length; --i >= 0;) {
					var child = doc.childNodes[i];
					if (child !== doc.documentElement) {
						doc.removeChild(child);
					}
				}
			}

			return doc;
		},
	};

	// Attach the standard DOM types to the global scope
	global.Node = Node;
	global.Comment = Comment;
	global.Document = Document;
	global.Element = Element;
	global.Text = Text;

	// Attach JSDOMParser to the global scope
	global.JSDOMParser = JSDOMParser;

})(this);
